// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:collection';

import 'package:analyzer/dart/analysis/declared_variables.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/dart/analysis/file_state.dart';
import 'package:analyzer/src/dart/analysis/restricted_analysis_context.dart';
import 'package:analyzer/src/dart/analysis/session.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/utilities_dart.dart';
import 'package:analyzer/src/summary/format.dart';
import 'package:analyzer/src/summary/idl.dart';
import 'package:analyzer/src/summary/link.dart';
import 'package:analyzer/src/summary/package_bundle_reader.dart';
import 'package:analyzer/src/summary/resynthesize.dart';
import 'package:analyzer/src/summary/summarize_ast.dart';
import 'package:analyzer/src/summary/summarize_elements.dart';
import 'package:analyzer/src/summary2/link.dart' as summary2;
import 'package:analyzer/src/summary2/linked_bundle_context.dart' as summary2;
import 'package:analyzer/src/summary2/linked_element_factory.dart' as summary2;
import 'package:analyzer/src/summary2/reference.dart' as summary2;
import 'package:meta/meta.dart';

class DevCompilerResynthesizerBuilder {
  final FileSystemState _fsState;
  final SourceFactory _sourceFactory;
  final DeclaredVariables _declaredVariables;
  final AnalysisOptionsImpl _analysisOptions;
  final SummaryDataStore _summaryData;
  final List<Uri> _explicitSources;

  _SourceCrawler _fileCrawler;

  final PackageBundleAssembler _assembler;
  List<int> summaryBytes;

  RestrictedAnalysisContext context;
  SummaryResynthesizer resynthesizer;
  summary2.LinkedElementFactory elementFactory;

  DevCompilerResynthesizerBuilder({
    @required FileSystemState fsState,
    @required SourceFactory sourceFactory,
    @required DeclaredVariables declaredVariables,
    @required AnalysisOptionsImpl analysisOptions,
    @required SummaryDataStore summaryData,
    @required List<Uri> explicitSources,
  })  : _fsState = fsState,
        _sourceFactory = sourceFactory,
        _declaredVariables = declaredVariables,
        _analysisOptions = analysisOptions,
        _summaryData = summaryData,
        _explicitSources = explicitSources,
        _assembler = PackageBundleAssembler();

  /// URIs of libraries that should be linked.
  List<String> get libraryUris => _fileCrawler.libraryUris;

  /// Link explicit sources, serialize [PackageBundle] into [summaryBytes].
  ///
  /// Create a new [context], [resynthesizer] and [elementFactory].
  void build() {
    _fileCrawler = _SourceCrawler(
      _fsState,
      _sourceFactory,
      _summaryData,
      _explicitSources,
    );
    _fileCrawler.crawl();

    _buildPackageBundleBytes();
    var bundle = PackageBundle.fromBuffer(summaryBytes);

    // Create an analysis context to contain the state for this build unit.
    var synchronousSession = SynchronousSession(
      _analysisOptions,
      _declaredVariables,
    );
    context = RestrictedAnalysisContext(synchronousSession, _sourceFactory);

    resynthesizer = StoreBasedSummaryResynthesizer(
      context,
      null,
      context.sourceFactory,
      /*strongMode*/ true,
      SummaryDataStore([])
        ..addStore(_summaryData)
        ..addBundle(null, bundle),
    );
    resynthesizer.finishCoreAsyncLibraries();
    context.typeProvider = resynthesizer.typeProvider;
  }

  void _buildPackageBundleBytes() {
    if (AnalysisDriver.useSummary2) {
      _computeLinkedLibraries2();
    } else {
      _computeLinkedLibraries1();
    }
    summaryBytes = _assembler.assemble().toBuffer();
  }

  void _computeLinkedLibraries1() {
    _fileCrawler.sourceToUnlinkedUnit.forEach((source, unlinkedUnit) {
      _assembler.addUnlinkedUnit(source, unlinkedUnit);
    });

    var linkResult = link(
        _fileCrawler.libraryUris.toSet(),
        (uri) => _summaryData.linkedMap[uri],
        (uri) =>
            _summaryData.unlinkedMap[uri] ??
            _fileCrawler.uriToUnlinkedUnit[uri],
        _declaredVariables,
        _analysisOptions);
    linkResult.forEach(_assembler.addLinkedLibrary);
  }

  /// Link libraries, and fill [_assembler].
  void _computeLinkedLibraries2() {
    var inputLibraries = <summary2.LinkInputLibrary>[];

    var sourceToUnit = _fileCrawler.sourceToUnit;
    for (var librarySource in sourceToUnit.keys) {
      var unit = sourceToUnit[librarySource];

      if (_explicitSources.contains(librarySource.uri)) {
        var isPart = unit.directives.any((d) => d is PartOfDirective);
        if (isPart) {
          continue;
        }
      }

      var inputUnits = <summary2.LinkInputUnit>[];
      inputUnits.add(
        summary2.LinkInputUnit(null, librarySource, false, unit),
      );

      for (var directive in unit.directives) {
        if (directive is PartDirective) {
          var partUri = directive.uri.stringValue;
          var partSource = _sourceFactory.resolveUri(librarySource, partUri);
          if (partSource != null) {
            var partUnit = sourceToUnit[partSource];
            inputUnits.add(
              summary2.LinkInputUnit(partUri, partSource, false, partUnit),
            );
          }
        }
      }

      inputLibraries.add(
        summary2.LinkInputLibrary(librarySource, inputUnits),
      );
    }

    var analysisContext = RestrictedAnalysisContext(
      SynchronousSession(_analysisOptions, _declaredVariables),
      _sourceFactory,
    );

    var elementFactory = summary2.LinkedElementFactory(
      analysisContext,
      null,
      summary2.Reference.root(),
    );

    for (var bundle in _summaryData.bundles) {
      elementFactory.addBundle(
        summary2.LinkedBundleContext(elementFactory, bundle.bundle2),
      );
    }

    var linkResult = summary2.link(elementFactory, inputLibraries);
    _assembler.setBundle2(linkResult.bundle);
  }
}

class _SourceCrawler {
  final FileSystemState _fsState;
  final SourceFactory _sourceFactory;
  final SummaryDataStore _summaryData;
  final List<Uri> _explicitSources;

  /// The pending list of sources to visit.
  var _pendingSource = Queue<Uri>();

  /// The sources that have been added to [_pendingSource], used to ensure
  /// we only visit a given source once.
  var _knownSources = Set<Uri>();

  final Map<Source, UnlinkedUnitBuilder> sourceToUnlinkedUnit = {};
  final Map<String, UnlinkedUnitBuilder> uriToUnlinkedUnit = {};
  final Map<Source, CompilationUnit> sourceToUnit = {};
  final List<String> libraryUris = [];

  _SourceCrawler(
    this._fsState,
    this._sourceFactory,
    this._summaryData,
    this._explicitSources,
  );

  /// Starting with [_explicitSources], visit all transitive imports, exports,
  /// parts, and create an unlinked unit for each (unless it is provided by an
  /// input summary from [_summaryData]).
  void crawl() {
    _pendingSource.addAll(_explicitSources);
    _knownSources.addAll(_explicitSources);

    // Collect the unlinked units for all transitive sources.
    //
    // TODO(jmesserly): consider using parallelism via asynchronous IO here,
    // once we fix debugger extension (web/web_command.dart) to allow async.
    //
    // It would let computation tasks (parsing/serializing unlinked units)
    // proceed in parallel with reading the sources from disk.
    while (_pendingSource.isNotEmpty) {
      _visit(_pendingSource.removeFirst());
    }
  }

  /// Visit the file with the given [uri], and fill its data.
  void _visit(Uri uri) {
    var uriStr = uri.toString();

    // Maybe an input package contains the source.
    if (_summaryData.unlinkedMap[uriStr] != null) {
      return;
    }

    var source = _sourceFactory.forUri2(uri);
    if (source == null) {
      return;
    }

    var file = _fsState.getFileForPath(source.fullName);
    var unit = file.parse();
    sourceToUnit[source] = unit;

    var unlinkedUnit = serializeAstUnlinked(unit);
    uriToUnlinkedUnit[uriStr] = unlinkedUnit;
    sourceToUnlinkedUnit[source] = unlinkedUnit;

    void enqueueSource(String relativeUri) {
      var sourceUri = resolveRelativeUri(uri, Uri.parse(relativeUri));
      if (_knownSources.add(sourceUri)) {
        _pendingSource.add(sourceUri);
      }
    }

    // Add reachable imports/exports/parts, if any.
    var isPart = false;
    for (var directive in unit.directives) {
      if (directive is UriBasedDirective) {
        enqueueSource(directive.uri.stringValue);
        // Handle conditional imports.
        if (directive is NamespaceDirective) {
          for (var config in directive.configurations) {
            enqueueSource(config.uri.stringValue);
          }
        }
      } else if (directive is PartOfDirective) {
        isPart = true;
      }
    }

    // Remember library URIs, for linking and compiling.
    if (!isPart) {
      libraryUris.add(uriStr);
    }
  }
}
